// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <tuple>

#include "base/macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/common/accessibility_messages.h"
#include "content/common/frame_messages.h"
#include "content/common/site_isolation_policy.h"
#include "content/common/view_message_enums.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/render_view_test.h"
#include "content/renderer/accessibility/render_accessibility_impl.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_view_impl.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/WebKit/public/platform/WebSize.h"
#include "third_party/WebKit/public/web/WebAXObject.h"
#include "third_party/WebKit/public/web/WebDocument.h"
#include "third_party/WebKit/public/web/WebView.h"
#include "ui/accessibility/ax_node_data.h"

using blink::WebAXObject;
using blink::WebDocument;

namespace content {

class TestRenderAccessibilityImpl : public RenderAccessibilityImpl {
public:
    explicit TestRenderAccessibilityImpl(RenderFrameImpl* render_frame)
        : RenderAccessibilityImpl(render_frame, ACCESSIBILITY_MODE_COMPLETE)
    {
    }

    void SendPendingAccessibilityEvents()
    {
        RenderAccessibilityImpl::SendPendingAccessibilityEvents();
    }
};

class RenderAccessibilityImplTest : public RenderViewTest {
public:
    RenderAccessibilityImplTest() { }

    RenderViewImpl* view()
    {
        return static_cast<RenderViewImpl*>(view_);
    }

    RenderFrameImpl* frame()
    {
        return static_cast<RenderFrameImpl*>(view()->GetMainRenderFrame());
    }

    void SetUp() override
    {
        RenderViewTest::SetUp();
        sink_ = &render_thread_->sink();
    }

    void TearDown() override
    {
#if defined(LEAK_SANITIZER)
        // Do this before shutting down V8 in RenderViewTest::TearDown().
        // http://crbug.com/328552
        __lsan_do_leak_check();
#endif
        RenderViewTest::TearDown();
    }

    void SetMode(AccessibilityMode mode)
    {
        frame()->OnSetAccessibilityMode(mode);
    }

    void GetAllAccEvents(
        std::vector<AccessibilityHostMsg_EventParams>* param_list)
    {
        const IPC::Message* message = sink_->GetUniqueMessageMatching(AccessibilityHostMsg_Events::ID);
        ASSERT_TRUE(message);
        std::tuple<std::vector<AccessibilityHostMsg_EventParams>, int, int> param;
        AccessibilityHostMsg_Events::Read(message, &param);
        *param_list = std::get<0>(param);
    }

    void GetLastAccEvent(
        AccessibilityHostMsg_EventParams* params)
    {
        std::vector<AccessibilityHostMsg_EventParams> param_list;
        GetAllAccEvents(&param_list);
        ASSERT_GE(param_list.size(), 1U);
        *params = param_list[0];
    }

    int CountAccessibilityNodesSentToBrowser()
    {
        AccessibilityHostMsg_EventParams event;
        GetLastAccEvent(&event);
        return event.update.nodes.size();
    }

protected:
    IPC::TestSink* sink_;

    DISALLOW_COPY_AND_ASSIGN(RenderAccessibilityImplTest);
};

TEST_F(RenderAccessibilityImplTest, SendFullAccessibilityTreeOnReload)
{
    // The job of RenderAccessibilityImpl is to serialize the
    // accessibility tree built by WebKit and send it to the browser.
    // When the accessibility tree changes, it tries to send only
    // the nodes that actually changed or were reparented. This test
    // ensures that the messages sent are correct in cases when a page
    // reloads, and that internal state is properly garbage-collected.
    std::string html = "<body>"
                       "  <div role='group' id='A'>"
                       "    <div role='group' id='A1'></div>"
                       "    <div role='group' id='A2'></div>"
                       "  </div>"
                       "</body>";
    LoadHTML(html.c_str());

    // Creating a RenderAccessibilityImpl should sent the tree to the browser.
    std::unique_ptr<TestRenderAccessibilityImpl> accessibility(
        new TestRenderAccessibilityImpl(frame()));
    accessibility->SendPendingAccessibilityEvents();
    EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());

    // If we post another event but the tree doesn't change,
    // we should only send 1 node to the browser.
    sink_->ClearMessages();
    WebDocument document = view()->GetWebView()->mainFrame()->document();
    WebAXObject root_obj = document.accessibilityObject();
    accessibility->HandleAXEvent(
        root_obj,
        ui::AX_EVENT_LAYOUT_COMPLETE);
    accessibility->SendPendingAccessibilityEvents();
    EXPECT_EQ(1, CountAccessibilityNodesSentToBrowser());
    {
        // Make sure it's the root object that was updated.
        AccessibilityHostMsg_EventParams event;
        GetLastAccEvent(&event);
        EXPECT_EQ(root_obj.axID(), event.update.nodes[0].id);
    }

    // If we reload the page and send a event, we should send
    // all 4 nodes to the browser. Also double-check that we didn't
    // leak any of the old BrowserTreeNodes.
    LoadHTML(html.c_str());
    document = view()->GetWebView()->mainFrame()->document();
    root_obj = document.accessibilityObject();
    sink_->ClearMessages();
    accessibility->HandleAXEvent(
        root_obj,
        ui::AX_EVENT_LAYOUT_COMPLETE);
    accessibility->SendPendingAccessibilityEvents();
    EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());

    // Even if the first event is sent on an element other than
    // the root, the whole tree should be updated because we know
    // the browser doesn't have the root element.
    LoadHTML(html.c_str());
    document = view()->GetWebView()->mainFrame()->document();
    root_obj = document.accessibilityObject();
    sink_->ClearMessages();
    const WebAXObject& first_child = root_obj.childAt(0);
    accessibility->HandleAXEvent(
        first_child,
        ui::AX_EVENT_LIVE_REGION_CHANGED);
    accessibility->SendPendingAccessibilityEvents();
    EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());
}

TEST_F(RenderAccessibilityImplTest, HideAccessibilityObject)
{
    // Test RenderAccessibilityImpl and make sure it sends the
    // proper event to the browser when an object in the tree
    // is hidden, but its children are not.
    std::string html = "<body>"
                       "  <div role='group' id='A'>"
                       "    <div role='group' id='B'>"
                       "      <div role='group' id='C' style='visibility:visible'>"
                       "      </div>"
                       "    </div>"
                       "  </div>"
                       "</body>";
    LoadHTML(html.c_str());

    std::unique_ptr<TestRenderAccessibilityImpl> accessibility(
        new TestRenderAccessibilityImpl(frame()));
    accessibility->SendPendingAccessibilityEvents();
    EXPECT_EQ(4, CountAccessibilityNodesSentToBrowser());

    WebDocument document = view()->GetWebView()->mainFrame()->document();
    WebAXObject root_obj = document.accessibilityObject();
    WebAXObject node_a = root_obj.childAt(0);
    WebAXObject node_b = node_a.childAt(0);
    WebAXObject node_c = node_b.childAt(0);

    // Hide node 'B' ('C' stays visible).
    ExecuteJavaScriptForTests(
        "document.getElementById('B').style.visibility = 'hidden';");
    // Force layout now.
    ExecuteJavaScriptForTests("document.getElementById('B').offsetLeft;");

    // Send a childrenChanged on 'A'.
    sink_->ClearMessages();
    accessibility->HandleAXEvent(
        node_a,
        ui::AX_EVENT_CHILDREN_CHANGED);

    accessibility->SendPendingAccessibilityEvents();
    AccessibilityHostMsg_EventParams event;
    GetLastAccEvent(&event);
    ASSERT_EQ(2U, event.update.nodes.size());

    // RenderAccessibilityImpl notices that 'C' is being reparented,
    // so it clears the subtree rooted at 'A', then updates 'A' and then 'C'.
    EXPECT_EQ(node_a.axID(), event.update.node_id_to_clear);
    EXPECT_EQ(node_a.axID(), event.update.nodes[0].id);
    EXPECT_EQ(node_c.axID(), event.update.nodes[1].id);
    EXPECT_EQ(2, CountAccessibilityNodesSentToBrowser());
}

TEST_F(RenderAccessibilityImplTest, ShowAccessibilityObject)
{
    // Test RenderAccessibilityImpl and make sure it sends the
    // proper event to the browser when an object in the tree
    // is shown, causing its own already-visible children to be
    // reparented to it.
    std::string html = "<body>"
                       "  <div role='group' id='A'>"
                       "    <div role='group' id='B' style='visibility:hidden'>"
                       "      <div role='group' id='C' style='visibility:visible'>"
                       "      </div>"
                       "    </div>"
                       "  </div>"
                       "</body>";
    LoadHTML(html.c_str());

    std::unique_ptr<TestRenderAccessibilityImpl> accessibility(
        new TestRenderAccessibilityImpl(frame()));
    accessibility->SendPendingAccessibilityEvents();
    EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser());

    // Show node 'B', then send a childrenChanged on 'A'.
    ExecuteJavaScriptForTests(
        "document.getElementById('B').style.visibility = 'visible';");
    ExecuteJavaScriptForTests("document.getElementById('B').offsetLeft;");

    sink_->ClearMessages();
    WebDocument document = view()->GetWebView()->mainFrame()->document();
    WebAXObject root_obj = document.accessibilityObject();
    WebAXObject node_a = root_obj.childAt(0);
    WebAXObject node_b = node_a.childAt(0);
    WebAXObject node_c = node_b.childAt(0);

    accessibility->HandleAXEvent(
        node_a,
        ui::AX_EVENT_CHILDREN_CHANGED);

    accessibility->SendPendingAccessibilityEvents();
    AccessibilityHostMsg_EventParams event;
    GetLastAccEvent(&event);

    ASSERT_EQ(3U, event.update.nodes.size());
    EXPECT_EQ(node_a.axID(), event.update.node_id_to_clear);
    EXPECT_EQ(node_a.axID(), event.update.nodes[0].id);
    EXPECT_EQ(node_b.axID(), event.update.nodes[1].id);
    EXPECT_EQ(node_c.axID(), event.update.nodes[2].id);
    EXPECT_EQ(3, CountAccessibilityNodesSentToBrowser());
}

} // namespace content
